﻿#region LGPL License

/*
OpenSearchNet
Copyright (c) 2014, Brad Hards <bradh@frogmouth.net>, All rights reserved.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library.
*/

#endregion LGPL License

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace OpenSearch
{
    /// <summary>
    /// Response to an OpenSearch query
    /// </summary>
    /// <remarks>
    /// <para>
    /// This class only makes sense for an Atom (<c>application/atom+xml</c>)
    /// or RSS2 (<c>application/rss+xml</c>) based query response.
    /// </para>
    /// </remarks>
    public class Response
    {
        private System.ServiceModel.Syndication.SyndicationFeed feed = null;

        private Collection<ResponseItem> responseItems = null;

        /// <summary>
        /// Create a new Reponse from the server response stream
        /// </summary>
        /// <param name="responseStream">Stream to parse, containing XML response</param>
        /// <remarks>
        /// <para>
        /// A Response is typically created from the stream associated with <c>System.Net.WebResponse</c>
        /// </para>
        /// </remarks>
        /// <example>
        /// <code>
        /// // Create a request for the URL.
        /// System.Net.WebRequest request = System.Net.WebRequest.Create(query);
        /// // If required by the server, set the credentials.
        /// request.Credentials = System.Net.CredentialCache.DefaultCredentials;
        /// // Get the response.
        /// System.Net.HttpWebResponse httpResponse = (System.Net.HttpWebResponse)request.GetResponse();
        /// // Create the Response
        /// OpenSearch.Response response = new OpenSearchResponse(httpResponse.GetStream());
        /// </code>
        /// </example>
        public Response(System.IO.Stream responseStream)
        {
            using (DateFixerXmlTextReader reader = new DateFixerXmlTextReader(responseStream))
            {
                feed = System.ServiceModel.Syndication.SyndicationFeed.Load(reader);
            }
        }

        /// <summary>
        /// Create a new Reponse from a string
        /// </summary>
        /// <param name="responseAsXml">string to load the description document contents from</param>
        public Response(string responseAsXml)
        {
            System.IO.StringReader sr = new System.IO.StringReader(responseAsXml);
            try
            {
                DateFixerXmlTextReader reader = new DateFixerXmlTextReader(sr);
                feed = System.ServiceModel.Syndication.SyndicationFeed.Load(reader);
            }
            finally
            {
                sr.Dispose();
            }
        }

        /// <summary>
        /// Title of the search response
        /// </summary>
        public string Title
        {
            get
            {
                try
                {
                    return feed.Title.Text;
                }
                catch (NullReferenceException)
                {
                    return "";
                }
            }
        }

        /// <summary>
        /// Unique identifier for the search response
        /// </summary>
        /// <remarks>
        /// This is typically a URN for Atom format responses, so you probably only want to display it to the user in a debugging scenario.
        /// </remarks>
        public string Id
        {
            get
            {
                try
                {
                    return feed.Id;
                }
                catch (NullReferenceException)
                {
                    return "";
                }
            }
        }

        /// <summary>
        /// Generator of the search response
        /// </summary>
        /// <remarks>
        /// <para>
        /// This corresponds to the <c>generator</c> element in an OpenSearch response, which is part of the Atom or RSS namespace (as applicable
        /// to the response format).
        /// </para>
        /// <para>
        /// The server does not need to provide this information. If it is not provided, an empty string will be returned.
        /// </para>
        /// </remarks>
        public string Generator
        {
            get
            {
                try
                {
                    return feed.Generator;
                }
                catch (NullReferenceException)
                {
                    return "";
                }
            }
        }

        /// <summary>
        /// Description for the search response
        /// </summary>
        /// <remarks>
        /// <para>
        /// This corresponds to the <c>description</c> element in an OpenSearch response, which is part of the Atom or RSS namespace (as applicable
        /// to the response format).
        /// </para>
        /// <para>
        /// The server does not need to provide this information. If it is not provided, an empty string will be returned.
        /// </para>
        /// </remarks>
        public string Description
        {
            get
            {
                try
                {
                    return feed.Description.Text;
                }
                catch (NullReferenceException)
                {
                    return "";
                }
            }
        }

        /// <summary>
        /// Authors for the search response
        /// </summary>
        /// <remarks>
        /// This usually corresponds to the <c>Author</c> element(s) in an OpenSearch response, which is part of the Atom namespace. The server
        /// does not need to provide this information. If it is not provided, an empty list will be returned.
        /// </remarks>
        public System.Collections.ObjectModel.Collection<System.ServiceModel.Syndication.SyndicationPerson> Authors
        {
            get
            {
                try
                {
                    return feed.Authors;
                }
                catch (NullReferenceException)
                {
                    return new System.Collections.ObjectModel.Collection<System.ServiceModel.Syndication.SyndicationPerson>();
                }
            }
        }

        /// <summary>
        /// Search results in processed form
        /// </summary>
        /// <remarks>
        /// This property provides the items returned by the search as an enumeration of <c>IResponseItem</c>, which
        /// provides high level access. For low level access to the underlying data, consider using ItemsRaw.
        /// </remarks>
        public IEnumerable<ResponseItem> Items
        {
            get
            {
                if (responseItems == null)
                {
                    buildResponseItems();
                }
                return responseItems;
            }
        }

        private void buildResponseItems()
        {
            responseItems = new Collection<ResponseItem>();
            foreach (System.ServiceModel.Syndication.SyndicationItem rawItem in feed.Items)
            {
                responseItems.Add(new ResponseItem(rawItem));
            }
        }

        /// <summary>
        /// Search results in "raw" form
        /// </summary>
        /// <remarks>
        /// This property provides the items returned by the search as an enumeration of <c>System.ServiceModel.Syndication.SyndicationItem</c>, which
        /// provides low-level access to the items for further processing. For an easier option, consider using Items.
        /// </remarks>
        public IEnumerable<System.ServiceModel.Syndication.SyndicationItem> ItemsRaw
        {
            get { return feed.Items; }
        }

        /// <summary>
        /// Number of search results available for the current search.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This corresponds to the <c>totalResults</c> element in an OpenSearch response.
        /// </para>
        /// <para>
        /// The server does not need to provide this information. If it is not provided, the client should consider the current
        /// page to be the last page of search results. To support this, if the <c>totalResults</c> element is not provided,
        /// this property will return 0.
        /// </para>
        /// </remarks>
        public int TotalResponseCount
        {
            get
            {
                try
                {
                    return feed.ElementExtensions.ReadElementExtensions<int>("totalResults", Constants.NSOpenSearch)[0];
                }
                catch (NullReferenceException)
                {
                    return 0;
                }
                catch (ArgumentOutOfRangeException)
                {
                    return 0;
                }
            }
        }

        /// <summary>
        /// Index for the first search result in the current set of search results.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Since results can be paged by the server, this property provides the offset of the first result. For example, if the search
        /// returns chunks of 10 items (i.e. ItemsPerPage == 10), then the first set of results will have items 1 to 10 (StartIndex of 1),
        /// the second set of results will have items 11 to 20 (StartIndex of 11), the third set of results will have items 21 to 30
        /// (StartIndex of 21) and so on.
        /// </para>
        /// <para>
        /// This corresponds to the <c>startIndex</c> element in an OpenSearch response.
        /// </para>
        /// <para>
        /// The server does not need to provide this information. If it is not provided, the client should consider the current
        /// page to be the first page of search results. To support this, if the <c>startIndex</c> element is not provided, this
        /// property will return 1.
        /// </para>
        /// </remarks>
        public int StartIndex
        {
            get
            {
                try
                {
                    return feed.ElementExtensions.ReadElementExtensions<int>("startIndex", Constants.NSOpenSearch)[0];
                }
                catch (NullReferenceException)
                {
                    return 1;
                }
                catch (ArgumentOutOfRangeException)
                {
                    return 1;
                }
            }
        }

        /// <summary>
        /// Number of search results returned per page (page size)
        /// </summary>
        /// <remarks>
        /// <para>This corresponds to the <c>itemPerPage</c> element in an OpenSearch response.</para>
        /// <para>
        /// The server does not need to provide this information. If it is not provided, this property will return 0.
        /// </para>
        /// <para>
        /// If you need a default page size, use the number of items on the current page. One way to get this information is to use <c>ItemsRaw.Count()</c>.
        /// </para>
        /// </remarks>
        public int ItemsPerPage
        {
            get
            {
                try
                {
                    return feed.ElementExtensions.ReadElementExtensions<int>("itemsPerPage", Constants.NSOpenSearch)[0];
                }
                catch (NullReferenceException)
                {
                    return 0;
                }
                catch (ArgumentOutOfRangeException)
                {
                    return 0;
                }
            }
        }

        /// <summary>
        /// Search queries that the search client can use
        /// </summary>
        /// <remarks>
        /// <para>This corresponds to the <c>Query</c> elements in an OpenSearch response.</para>
        /// </remarks>
        public IEnumerable<Query> Queries
        {
            get
            {
                try
                {
                    List<Query> queries = new List<Query>();
                    foreach (System.Xml.XmlElement queryElement in feed.ElementExtensions.ReadElementExtensions<System.Xml.XmlElement>("Query", Constants.NSOpenSearch))
                    {
                        queries.Add(new Query(queryElement));
                    }
                    return queries;
                }
                catch (NullReferenceException)
                {
                    return new System.Collections.ObjectModel.Collection<Query>() as IEnumerable<Query>;
                }
            }
        }

        /// <summary>
        /// Categories of the search results
        /// </summary>
        public Collection<System.ServiceModel.Syndication.SyndicationCategory> Categories
        {
            get { return feed.Categories; }
        }

        /// <summary>
        /// Links in the search results
        /// </summary>
        /// <remarks>
        /// <para>
        /// Links here apply to the search results as a whole. There can also be links
        /// on individual search items.
        /// </para>
        /// </remarks>
        public Collection<Link> Links
        {
            get
            {
                try
                {
                    Collection<Link> links = new Collection<Link>();
                    foreach (System.ServiceModel.Syndication.SyndicationLink rawLink in feed.Links)
                    {
                        links.Add(new Link(rawLink));
                    }
                    return links;
                }
                catch (NullReferenceException)
                {
                    return new Collection<Link>();
                }
            }
        }

        /// <summary>
        /// Language of the search results
        /// </summary>
        /// <remarks>
        /// <para>
        /// This should be an encoded form, such as <c>en-us</c>.
        /// </para>
        /// </remarks>
        public string Language
        {
            get
            {
                try
                {
                    return feed.Language;
                }
                catch (NullReferenceException)
                {
                    return "";
                }
            }
        }

        /// <summary>
        /// The date and time that the response was last updated (or was generated).
        /// </summary>
        /// <remarks>
        /// <para>
        /// This corresponds to the <c>updated</c> element in the Atom namespace or the <c>lastBuildDate</c> element in the RSS2 namespace. The server
        /// does not need to provide this information. If it is not provided, a default constructed (invalid) DateTimeOffset will be returned.
        /// </para>
        /// </remarks>
        public DateTimeOffset LastUpdated
        {
            get
            {
                try
                {
                    return feed.LastUpdatedTime;
                }
                catch (NullReferenceException)
                {
                    return new DateTimeOffset();
                }
            }
        }

        /// <summary>
        /// Access non-standard (extended elements) in the query response
        /// </summary>
        /// <typeparam name="T">Type of the response</typeparam>
        /// <param name="parameterNamespace">Namespace for the parameter to access</param>
        /// <param name="name">Name of the parameter to access</param>
        /// <returns>Collection (of type T) matching the extended elements by Namespace and Name</returns>
        public System.Collections.ObjectModel.Collection<T> GetExtendedElements<T>(string parameterNamespace, string name)
        {
            return feed.ElementExtensions.ReadElementExtensions<T>(name, parameterNamespace);
        }
    }
}